package com.region.loadbalancer;

import com.region.common.adapter.logging.LoggerAdapter;
import com.region.common.adapter.logging.LoggerAdapterFactory;
import com.region.common.utils.RandomUtils;
import com.region.loadbalancer.exception.NoLoadBalancerException;
import com.region.loadbalancer.execute.ServerExecute;
import com.region.loadbalancer.group.GroupServer;
import com.region.loadbalancer.group.GroupServerManager;
import com.region.loadbalancer.group.Server;
import com.region.loadbalancer.monitor.ServerMonitor;

import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * Load Balancing Manager
 *
 * @author liujieyu
 * @date 2023/5/24 23:30
 * @desciption
 */
public final class LoadBalancerManager {

    private final LoggerAdapter loggerAdapter = LoggerAdapterFactory.getLogger(getClass());

    private static LoadBalancerManager INSTANCE = new LoadBalancerManager();

    private LoadBalancerManager() {
        super();
    }

    public static LoadBalancerManager getInstance() {
        return INSTANCE;
    }

    public static LoadBalancerManager getNewInstance() {
        return new LoadBalancerManager();
    }

    // Service group containers
    private Map<String, GroupServer> groups = new ConcurrentHashMap<>();

    // The default config of load balancer manager
    private LoadBalancerConfig loadBalancerConfig;

    // Initialize or not
    private AtomicBoolean status = new AtomicBoolean(false);

    // Set load balancer config
    public void setLoadBalancerConfig(LoadBalancerConfig loadBalancerConfig) {
        this.loadBalancerConfig = loadBalancerConfig;
    }

    /**
     * Add GroupServer Object
     */
    public void addGroupServer(GroupServer groupServer) {
        if (loggerAdapter.isDebugEnabled()) {
            loggerAdapter.debug("The group server info is " + groupServer);
        }
        groups.putIfAbsent(groupServer.getGroupName(), groupServer);
    }

    /**
     * Parsing link information and adding to service groups
     * e.g. 127.0.0.1:3306
     *
     * @param groupName
     * @param socketInfos
     */
    public void addSocketInfo(String groupName, String... socketInfos) {
        List<Server> servers = new ArrayList<>();
        for (String socketInfo : socketInfos) {
            servers.add(parseSocketInfo(socketInfo.trim()));
        }
        addGroupServer(GroupServer.builder()
                .groupName(groupName)
                .servers(servers)
                .autoDetect(this.loadBalancerConfig.isAutoDetect())
                .detectionConnection(this.loadBalancerConfig.getDetectionConnection())
                .listeners(this.loadBalancerConfig.getListeners())
                .policy(this.loadBalancerConfig.getPolicy())
                .detectOnBorrow(this.loadBalancerConfig.isDetectOnBorrow())
                .build());
    }

    /**
     * Parsing link information and adding to service groups with no group name
     *
     * @param socketInfos
     * @return
     */
    public String addSocketInfoWithNoGroupName(String... socketInfos) {
        // Get 8-bit random string
        String groupName = RandomUtils.getRandomString(8);
        addSocketInfo(groupName, socketInfos);
        return groupName;
    }

    /**
     * Add URL
     *
     * @param groupName
     * @param urls
     */
    public void addURLServer(String groupName, URL... urls) {
        List<Server> servers = new ArrayList<>();
        for (URL url : urls) {
            servers.add(Server.builder()
                    .protocol(url.getProtocol())
                    .host(url.getHost())
                    .port(url.getPort())
                    .primePath(url.getPath())
                    .build());
        }
        addGroupServer(GroupServer.builder()
                .groupName(groupName)
                .servers(servers)
                .autoDetect(this.loadBalancerConfig.isAutoDetect())
                .detectionConnection(this.loadBalancerConfig.getDetectionConnection())
                .listeners(this.loadBalancerConfig.getListeners())
                .policy(this.loadBalancerConfig.getPolicy())
                .detectOnBorrow(this.loadBalancerConfig.isDetectOnBorrow())
                .build());
    }

    /**
     * Add URL with no group name
     *
     * @param urls
     * @return
     */
    public String addURLServerWithNoGroupName(URL... urls) {
        // Get 8-bit random string
        String groupName = RandomUtils.getRandomString(8);
        addURLServer(groupName, urls);
        return groupName;
    }

    /**
     * Get the specified service group manager
     *
     * @param groupName
     * @return
     */
    private GroupServerManager getGroupServerManager(String groupName) {
        if (this.groups.get(groupName) == null) {
            throw new NoLoadBalancerException("No load-balancing services found, the server name is " + groupName);
        }
        return this.groups.get(groupName).getManager();
    }

    /**
     * Load Balancing Manager Initialization
     */
    public void initialization() {
        if (status.getAndSet(true)) {
            return;
        }
        if (this.loadBalancerConfig == null) {
            this.loadBalancerConfig = new LoadBalancerConfig();
        }
        ServerMonitor.getInstance().setMonitorConfig(this.loadBalancerConfig.getMonitorCore(),
                this.loadBalancerConfig.getDetectionDuration(), this.loadBalancerConfig.getDetectionType());
        ServerMonitor.getInstance().loadSchedulerExecutor();
    }

    /**
     * Select service node by policy
     *
     * @param groupName
     * @return
     */
    public Server chooseServer(String groupName) {
        return getGroupServerManager(groupName).chooseServer();
    }

    /**
     * Select the available nodes using the carry in executor
     * @param groupName
     * @param execute
     * @param <T>
     * @return
     */
    public <T> T chooseServerWithExecute(String groupName, ServerExecute<T> execute) {
        Server server = null;
        while (true) {
            try {
                server = chooseServer(groupName);
                if (server == null) {
                    // no server
                    throw new NoLoadBalancerException("No load-balancing services found, the server name is " + groupName);
                }
                long startTime = System.nanoTime();
                T result = execute.execute(server);
                saveServerResponseTime(groupName, server, 1.0 * (System.nanoTime() - startTime) / (1000 * 1000));
                return result;
            } catch (Exception e) {
                if (e instanceof NoLoadBalancerException) {
                    throw e;
                }
                submitAnomalyServer(groupName, server);
                loggerAdapter.error("The server:" + server + " execute fail. Submit anomaly server to server monitor.", e);
            }
        }
    }

    /**
     * Submit anomaly server
     *
     * @param groupName
     * @param anomalyServer
     */
    public void submitAnomalyServer(String groupName, Server anomalyServer) {
        if (anomalyServer == null)
            return;
        getGroupServerManager(groupName).handleAnomalyServer(anomalyServer);
    }

    /**
     * Save the current response time of the service
     *
     * @param groupName
     * @param server
     * @param time
     */
    public void saveServerResponseTime(String groupName, Server server, double time) {
        getGroupServerManager(groupName).saveServerResponseTime(server, time);
    }

    /**
     * Parsing link information and generating server
     *
     * @param socketInfo
     * @return
     */
    private Server parseSocketInfo(String socketInfo) {
        String port = socketInfo.substring(socketInfo.lastIndexOf(":") + 1);
        String host = socketInfo.substring(0, socketInfo.lastIndexOf(":"));
        Server server = Server.builder().host(host).port(Integer.parseInt(port)).build();
        return server;
    }

    /**
     * Close server monitor
     */
    public void shutdownMonitor() {
        ServerMonitor.getInstance().shutdown();
    }

    /**
     * Check whether it is closed
     * @return
     */
    public boolean isShutdown() {
        return ServerMonitor.getInstance().isShutdown();
    }

}
